/* SOLENG.C - Solipeg engine */

#include <plib.h>
#include <solipeg.g>
#include <solipeg.rsg>
#include <hwim.h>

#define ASSERTIONS
#ifndef ASSERTIONS
    #pragma warn(wcne=>off)
#endif
#include <p_assert.h>

GLREF_D PR_APPMAN * const       w_am ;
GLREF_D PR_WSERV *              w_ws ;
GLREF_D TEXT *                  DatCommandPtr ;
GLREF_D TEXT *                  DatApp1 ;

#define N_VECTORS           8       /* directions of movement       */

#define DESIGN_SIZE         22      /* size in pixels of design     */

// Categories of pieces
enum {  S_UNUSABLE = 'a',
        S_EMPTY,
        S_FULL,
        S_MOVABLE } ;

// Offsets from tl of screen
#define BOARD_X(x)          (6 + (x) * (DESIGN_SIZE - 1))
#define BOARD_Y(y)          (6 + (y) * (DESIGN_SIZE - 1))

// lValue MACROS
#define CX                  (self->soleng.cx)
#define CY                  (self->soleng.cy)
#define BOARD               (self->soleng.board)
#define SHAPE               (self->soleng.shape)
#define RULES               (self->soleng.rules)
#define SHOW                (self->soleng.show)
#define JUMPSTR             (self->soleng.strJump)
#define MOVESTR             (self->soleng.strMove)
#define hBMP                (self->soleng.hBMP)
#define hBMPGrey            (self->soleng.hBMPGrey)
#define hBMPSprite          (self->soleng.hBMPSprite)
#define iSpriteID           (self->soleng.iSpriteID)
#define PFCB                (self->soleng.pfcb)
#define wSpriteOffset       (self->soleng.wSpriteOffset)
#define ridDefault          (self->soleng.ridDefault)
#define SOUND               (self->soleng.sound)
#define WINPOS              (self->soleng.winpos)
// rValue MACROS
#define EDITING             (self->soleng.mode == 1)

#define INBOUNDS(x,y)       (   (x) >= 0 && (x) < BOARD_SIZE_X &&\
                                (y) >= 0 && (y) < BOARD_SIZE_Y )
#define USABLE(x,y)         ( INBOUNDS((x), (y)) && BOARD[(x)][(y)] >= S_EMPTY )
#define EMPTY(x,y)          ( INBOUNDS((x), (y)) && BOARD[(x)][(y)] == S_EMPTY )
#define FULL(x,y)           ( INBOUNDS((x), (y)) && BOARD[(x)][(y)] >= S_FULL )

enum {  DIR_NONE = -1,
        DIR_W = 0,
        DIR_NW,
        DIR_N,
        DIR_NE,
        DIR_E,
        DIR_SE,
        DIR_S,
        DIR_SW } ;


typedef struct {
    BYTE        x ;
    BYTE        y ;
} VECTOR ;

LOCAL_D VECTOR          vectors[N_VECTORS] = {  {-1,  0},
                                                {-1, -1},
                                                { 0, -1},
                                                { 1, -1},
                                                { 1,  0},
                                                { 1,  1},
                                                { 0,  1},
                                                {-1,  1} } ;


#pragma METHOD_CALL


// Place the cursor at the current position if a sprite exists
METHOD VOID soleng_eng_cursor(PR_SOLENG *self) {
    if ( iSpriteID ) {
        P_POINT     pos ;

        pos.x = wSpriteOffset + BOARD_X(CX) ;
        pos.y = BOARD_Y(CY) ;
        wSetSprite(iSpriteID, &pos, 0, NULL) ;
    }
}

// Change the offset (sprites are offset from TL of SCREEN)
METHOD VOID soleng_eng_setcursor(PR_SOLENG *self, WORD wOffset) {
    wSpriteOffset = wOffset ;
    soleng_eng_cursor(self) ;
}

// Draw the board in the gurrent GC (changing as necessary)
METHOD VOID soleng_eng_draw_board(PR_SOLENG *self) {
    G_GC        gc ;
    BYTE        x ;
    BYTE        y ;
    P_RECT      rect ;
    P_POINT     pos ;
    P_RECT      rectPieces[] =  {   {   {0, 0},
                                        {DESIGN_SIZE, DESIGN_SIZE} },
                                    {   {DESIGN_SIZE - 2, 0},
                                        {DESIGN_SIZE - 2 + DESIGN_SIZE, DESIGN_SIZE} },
                                    {   {DESIGN_SIZE - 2, 0},
                                        {DESIGN_SIZE - 2 + DESIGN_SIZE, DESIGN_SIZE} }
                                } ;

    if ( SHOW ) {
        rectPieces[2].tl.x += DESIGN_SIZE - 2 ;
        rectPieces[2].br.x += DESIGN_SIZE - 2 ;
    }

    gc.flags = 0 ;
    gSetGC(0, G_GC_MASK_GREY, &gc) ;

    pos.x = 6 ;
    for ( x = 0 ; x < BOARD_SIZE_X ; x++ ) {
        pos.y = 6 ;
        for ( y = 0 ; y < BOARD_SIZE_Y ; y++ ) {
            if ( BOARD[x][y] != S_UNUSABLE ) {
                gCopyBit(   &pos,
                            hBMP,
                            &rectPieces[BOARD[x][y] - S_EMPTY],
                            G_TRMODE_REPL ) ;
            }
            pos.y += DESIGN_SIZE - 1 ;
        }
        pos.x += DESIGN_SIZE - 1 ;
    }

    gc.flags = G_GC_FLAG_GREY_PLANE ;
    gSetGC(0, G_GC_MASK_GREY, &gc) ;

    pos.x = 6 ;
    for ( x = 0 ; x < BOARD_SIZE_X ; x++ ) {
        pos.y = 6 ;
        for ( y = 0 ; y < BOARD_SIZE_Y ; y++ ) {
            if ( BOARD[x][y] != S_UNUSABLE ) {
                gCopyBit(   &pos,
                            hBMPGrey,
                            &rectPieces[BOARD[x][y] - S_EMPTY],
                            G_TRMODE_REPL ) ;
            }
            pos.y += DESIGN_SIZE - 1 ;
        }
        pos.x += DESIGN_SIZE - 1 ;
    }

    gc.flags = G_GC_FLAG_BOTH_PLANES ;
    gSetGC(0, G_GC_MASK_GREY, &gc) ;

    rect.tl.x = 6 ;
    rect.br.x = 6 + DESIGN_SIZE ;
    for ( x = 0 ; x < BOARD_SIZE_X ; x++ ) {
        rect.tl.y = 6 ;
        rect.br.y = 6 + DESIGN_SIZE ;
        for ( y = 0 ; y < BOARD_SIZE_Y ; y++ ) {
            if ( BOARD[x][y] == S_UNUSABLE ) {
                /* Clear */
                gClrRect(&rect, G_TRMODE_CLR) ;
            }
            rect.tl.y += DESIGN_SIZE - 1 ;
            rect.br.y += DESIGN_SIZE - 1 ;
        }
        rect.tl.x += DESIGN_SIZE - 1 ;
        rect.br.x += DESIGN_SIZE - 1 ;
    }

}

// Update (internal and graphical) board to reflect changes due to movement
METHOD INT soleng_eng_update_board(PR_SOLENG *self) {
    BYTE    x ;
    BYTE    y ;
    BYTE    iLp ;
    INT     fNoMove = TRUE ;
    INT     iIncrement ;

    iIncrement = RULES ? 1 : 2 ;

	for ( x = 0 ; x < BOARD_SIZE_X ; x++ ) {
		for ( y = 0 ; y < BOARD_SIZE_Y ; y++ ) {
            if ( BOARD[x][y] >= S_FULL ) {
                BOARD[x][y] = S_FULL ;
                for ( iLp = 0 ; iLp < N_VECTORS ; iLp += iIncrement ) {
					if (	FULL(x + vectors[iLp].x, y + vectors[iLp].y) &&
                            EMPTY(x + 2 * vectors[iLp].x, y + 2 * vectors[iLp].y) ) {
                        BOARD[x][y] = S_MOVABLE ;
                        fNoMove = FALSE ;
                        break ;
					}
				}
			}
        }
	}

    if ( !USABLE(CX, CY) ) {
        CX = 3 ;
        CY = 5 ;
    }

    gCreateTempGC0(w_ws->wserv.cli->win.id) ;
    soleng_eng_draw_board(self) ;
    gFreeTempGC() ;

    return( fNoMove && !EDITING ) ;
}

// Reset the game according to the default starting position
METHOD VOID soleng_eng_new_game(PR_SOLENG *self) {
    VOID *      pbuf = NULL ;

    CX = 3 ;
    CY = 5 ;
    hLoadResource(SG_PL + ridDefault, &pbuf) ;
    p_bcpy(BOARD, pbuf, 49) ;
    p_free(pbuf) ;
    soleng_eng_update_board(self) ;
    soleng_eng_cursor(self) ;
}

// Change the shape of the board
METHOD VOID soleng_eng_shape(PR_SOLENG *self) {
    ridDefault = SHAPE ;
    soleng_eng_new_game(self) ;
}

// Make a cursor (should be called only once)
METHOD VOID soleng_eng_make_cursor(PR_SOLENG *self) {
    W_SPRITE    sprite[1] = {   {   0, 0, 0,
                                    0, 0, 0,
                                    {0, 0},
                                    0 }
                            } ;
    P_RECT      rect = {    {0, 0},
                            {DESIGN_SIZE, DESIGN_SIZE} } ;

    __ASSERT( iSpriteID == 0 ) ;
    hBMPSprite = gCreateBit(0, &rect.br) ;
    gCreateTempGC0(hBMPSprite) ;
    gClrRect(&rect, G_TRMODE_CLR) ;
    gBorder(W_BORD_CORNER_1) ;
    gFreeTempGC() ;

    rect.br.x = wSpriteOffset + BOARD_X(CX) ;
    rect.br.y = BOARD_Y(CY) ;
    sprite[0].bit_set = hBMPSprite ;
    iSpriteID = wCreateSprite(  w_ws->wserv.cli->win.id,
                                &rect.br,
                                0,
                                1,
                                sprite ) ;
}

// Engine initialization
METHOD VOID soleng_eng_init(PR_SOLENG *self) {
    TEXT *      strList[2] ;
    TEXT        strFilename[P_FNAMESIZE] = "" ;
    P_POINT     pt ;

    hLoadResource(RS_PLAY, &strList[0]) ;
    hLoadResource(RS_EDIT, &strList[1]) ;
    wsSetList(self->soleng.nModes = 2, strList, self->soleng.mode = 0) ;
    p_free(strList[0]) ;
    p_free(strList[1]) ;
    WINPOS = SHOW = SOUND = 1 ;
    p_getenviron(DatApp1, p_slen(DatApp1), &self->soleng.show) ;
    hLoadResource(RS_JUMP_STR, &JUMPSTR) ;
    hLoadResource(RS_MOVE_STR, &MOVESTR) ;

    f_fparse(strFilename, DatCommandPtr, strFilename, NULL) ;
    hBMP = gOpenBit(strFilename, 2, 0, &pt) ;
    hBMPGrey = gOpenBit(strFilename, 3, 0, &pt) ;
    self->soleng.waveao = f_newsend(    CAT_SOLIPEG_SOLIPEG,
                                        C_WAVEAO,
                                        O_AO_INIT ) ;
}

// Mode change due to diamond key
METHOD VOID soleng_eng_mode_change(PR_SOLENG *self, INT back) {
    if ( back ) {
        if ( --self->soleng.mode < 0 ) {
            self->soleng.mode = self->soleng.nModes - 1 ;
        }
    }
    else {
        if ( ++self->soleng.mode >= self->soleng.nModes ) {
            self->soleng.mode = 0 ;
        }
    }
    wsSelectList(self->soleng.mode) ;
}

// Read in a new game from the specified file
METHOD VOID soleng_eng_open_game(PR_SOLENG *self, TEXT *strFilename) {
    BYTE    buf[BOARD_SIZE_X * BOARD_SIZE_Y] ;
    INT     iRet ;

    if ( PFCB != NULL ) {
        p_close(PFCB) ;
        PFCB = NULL ;
    }

    f_open(&PFCB, strFilename, P_FSTREAM | P_FOPEN) ;
    iRet = f_read(PFCB, buf, sizeof(buf)) ;
    p_close(PFCB) ;
    PFCB = NULL ;

    if ( iRet != sizeof(BOARD) ) {
        p_send5(w_am, O_AM_NOTIFY, RS_BADFILE, NULL, NULL) ;
        f_leave(E_GEN_FAIL) ;
    }

    for (   iRet = 0 ;
            iRet < 49 && buf[iRet] >= S_UNUSABLE && buf[iRet] <= S_MOVABLE ;
            iRet++ ) ;
    if ( iRet < 49 ) {
        p_send5(w_am, O_AM_NOTIFY, RS_BADFILE, NULL, NULL) ;
        f_leave(E_GEN_FAIL) ;
    }

    p_bcpy(BOARD, buf, 49) ;

    ridDefault = SHAPE = USABLE(1, 1) ;
}

// Save a game to the specified file
METHOD VOID soleng_eng_save_game(PR_SOLENG *self, TEXT *strFilename) {
    if ( PFCB != NULL ) {
        p_close(PFCB) ;
        PFCB = NULL ;
    }

    hEnsurePath(strFilename) ;
    f_open(&PFCB, strFilename, P_FSTREAM | P_FUPDATE | P_FREPLACE) ;
    f_write(PFCB, BOARD, sizeof(BOARD)) ;
    p_close(PFCB) ;
    PFCB = NULL ;
}

// Load a game from an external library
METHOD VOID soleng_eng_external_library(    PR_SOLENG * self,
                                            PR_SOLEL *  pEL,
                                            INT         iIndex ) {
    TEXT *      pbuf = NULL ;
    INT         iLp ;

    CX = 3 ;
    CY = 5 ;
    ridDefault = 0 ;
    p_send4(pEL, O_EL_READ, &pbuf, iIndex) ;
    for (   iLp = 0 ;
            iLp < 49 && pbuf[iLp] >= S_UNUSABLE && pbuf[iLp] <= S_MOVABLE ;
            iLp++ ) ;
    if ( iLp >= 49 ) {
        p_bcpy(BOARD, pbuf, 49) ;
    }
    else {
        p_send5(w_am, O_AM_NOTIFY, RS_BADFILE, NULL, NULL) ;
    }
    p_free(pbuf) ;
    soleng_eng_update_board(self) ;
    soleng_eng_cursor(self) ;
}

// Beep (if sound enabled)
METHOD VOID soleng_eng_beep(PR_SOLENG *self, INT iResID) {
    if ( SOUND ) {
        TEXT        strName[P_FNAMESIZE] ;

        hLoadResBuf(iResID, strName) ;
        p_send2(self->soleng.waveao, O_AO_CANCEL) ;
        p_send4(self->soleng.waveao, O_WV_PLAY, strName, 0) ;
    }
}

METHOD INT soleng_eng_won(PR_SOLENG *self) {
    INT     iLp ;
    INT     jLp ;

    p_bcpy( &self->soleng.results[1],
            &self->soleng.results[0],
            sizeof(self->soleng.results) - sizeof(self->soleng.results[0])) ;

    self->soleng.results[0] = 0 ;

    for ( iLp = 0 ; iLp < BOARD_SIZE_X ; iLp++ ) {
        for ( jLp = 0 ; jLp < BOARD_SIZE_Y ; jLp++ ) {
            self->soleng.results[0] += FULL(iLp, jLp) ;
        }
    }

    self->soleng.total += self->soleng.results[0] ;
    self->soleng.time++ ;

    return( self->soleng.results[0] == 1 && FULL(3, 3) ) ;
}

// Handle keypresses
METHOD VOID soleng_eng_key(PR_SOLENG *self, INT keycode, INT mods) {
    INT     iDir = DIR_NONE ;
    INT     fJumpMove = FALSE ;

    if (    keycode == W_KEY_DELETE_LEFT &&
            mods == (W_SHIFT_MODIFIER | W_CTRL_MODIFIER) ) {
        p_delenv(DatApp1) ;
        p_exit(0) ;
    }

    switch ( keycode ) {

        case ' ' :
            if ( EDITING ) {
                BOARD[CX][CY] = (BOARD[CX][CY] == S_EMPTY ? S_FULL : S_EMPTY) ;
                soleng_eng_update_board(self) ;
            }
            else {
                soleng_eng_beep(self, RS_WARN_SOUND) ;
            }
            return ;

        case W_KEY_HOME :
            if ( USABLE(CX - 1, CY) ) {
                CX = 0 ;
                while ( !USABLE(CX, CY) ) CX++ ;
                soleng_eng_cursor(self) ;
            }
            return ;

        case W_KEY_END :
            if ( USABLE(CX + 1, CY) ) {
                CX = BOARD_SIZE_X - 1 ;
                while ( !USABLE(CX, CY) ) CX-- ;
                soleng_eng_cursor(self) ;
            }
            return ;

        case W_KEY_PAGE_UP :
            if ( USABLE(CX, CY - 1) ) {
                CY = 0 ;
                while ( !USABLE(CX, CY) ) CY++ ;
                soleng_eng_cursor(self) ;
            }
            return ;

        case W_KEY_PAGE_DOWN :
            if ( USABLE(CX, CY + 1) ) {
                CY = BOARD_SIZE_Y - 1 ;
                while ( !USABLE(CX, CY) ) CY-- ;
                soleng_eng_cursor(self) ;
            }
            return ;

        case W_KEY_UP :
            iDir = DIR_N ;
            break ;

        case W_KEY_DOWN :
            iDir = DIR_S ;
            break ;

        case W_KEY_RIGHT :
            iDir = DIR_E ;
            break ;

        case W_KEY_LEFT :
            iDir = DIR_W ;
            break ;

        default :
            iDir = p_sloc(self->soleng.strJump, keycode) ;

            if ( iDir != DIR_NONE ) {
                /* Jump Move */
                fJumpMove = RULES || !(iDir % 2) ;
                if ( !fJumpMove ) {
                    soleng_eng_beep(self, RS_WARN_SOUND) ;
                    return ;
                }
            }
            else {
                /* Move Move? */
                iDir = p_sloc(self->soleng.strMove, keycode) ;

                if ( iDir == DIR_NONE ) {
                    soleng_eng_beep(self, RS_WARN_SOUND) ;
                    return ;
                }
            }
            break ;
    }

    fJumpMove = fJumpMove || mods == W_SHIFT_MODIFIER ;

    if ( EDITING || !fJumpMove ) {
        if ( USABLE(CX + vectors[iDir].x, CY + vectors[iDir].y) ) {
            CX += vectors[iDir].x ;
            CY += vectors[iDir].y ;
            soleng_eng_cursor(self) ;
        }
    }
    else if (   BOARD[CX][CY] >= S_MOVABLE &&
                FULL(CX + vectors[iDir].x, CY + vectors[iDir].y) &&
                EMPTY(CX + 2 * vectors[iDir].x, CY + 2 * vectors[iDir].y) ) {
        BOARD[CX][CY] = S_EMPTY ;
        BOARD[CX + vectors[iDir].x][CY + vectors[iDir].y] = S_EMPTY ;
        BOARD[CX + 2 * vectors[iDir].x][CY + 2 * vectors[iDir].y] = S_FULL ;

        CX += 2 * vectors[iDir].x ;
        CY += 2 * vectors[iDir].y ;
        soleng_eng_cursor(self) ;
        soleng_eng_beep(self, RS_JUMP_SOUND) ;

        if ( soleng_eng_update_board(self) ) {
            soleng_eng_beep(self, soleng_eng_won(self) ? RS_WIN_SOUND : RS_FINIS_SOUND) ;

            if ( hConfirm(RS_NEW_GAME) ) {
                soleng_eng_new_game(self) ;
            }
            else {
                hWservComSend(O_COM_EXIT) ;
            }
        }
    }
    else {
        soleng_eng_beep(self, RS_WARN_SOUND) ;
    }
}

